package com.google.code.struts2jruby;

import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import ognl.OgnlRuntime;

import org.apache.log4j.Logger;
import org.jruby.Ruby;
import org.jruby.RubyObject;
import org.jruby.runtime.builtin.IRubyObject;

import com.google.code.struts2jruby.ognl.RubyObjectMethodAccessor;
import com.google.code.struts2jruby.ognl.RubyObjectPropertyAccessor;
import com.opensymphony.xwork2.ObjectFactory;
import com.opensymphony.xwork2.config.entities.ActionConfig;
import com.opensymphony.xwork2.inject.Inject;

/**
 * TODO: Document this class, and more importantly document the following
 * parameters
 * <ol>
 * <li>struts.jruby.basePackage - The base package the contains the Ruby
 * scripts</li>
 * <li>struts.jruby.autoReload - A boolean to enable/disable the auto reloading
 * of scripts.</li>
 * 
 * @author Richard L. Burton III
 */
public class JRubyObjectFactory extends ObjectFactory {

	private static Logger LOGGER = Logger.getLogger(JRubyObjectFactory.class);

	/**
	 * The base package to search for the Ruby scripts. This design follows the
	 * same design as the Zero configuration.
	 */
	protected String basePackage;

	/**
	 * A boolean flag to enable checking of the lastModified time of the Ruby
	 * scripts has changed between invocations.
	 */
	protected boolean autoReload;

	/**
	 * A boolean used to preload all Ruby scripts into the Ruby Runtime
	 * environment for better performance during the initial requests. TODO:
	 * Implement the usage of this attribute.
	 */
	protected boolean preload;

	/**
	 * The JRuby runtime instance.
	 */
	private Ruby ruby;

	/**
	 * The internal cache of the timestamps for each Script. Key: The Ruby
	 * Script. Value: lastModified time.
	 */
	private ConcurrentMap<String, Long> timestamps = new ConcurrentHashMap<String, Long>();

	public JRubyObjectFactory() {
		init();
	}

	/**
	 * The initialization step of this instance. This method is design for
	 * developers that will later extend this class and don't wish to copy paste
	 * existing code.
	 */
	protected void init() {
		OgnlRuntime.setPropertyAccessor(RubyObject.class,
				new RubyObjectPropertyAccessor());
		OgnlRuntime.setMethodAccessor(RubyObject.class,
				new RubyObjectMethodAccessor());
		ruby = Ruby.getDefaultInstance();
		LOGGER.info("Loaded JRubyObjectFactory!");
	}

	/**
	 * Returns an instance of a JRuby object by taking the namespace + the
	 * action name and prefixing it with the <tt>basePackage</tt> attribute.
	 */
	public IRubyObject getBeanInstance(String classPathResource,
			ActionConfig actionConfig, Map context) {
		InputStream in = this.getClass().getResourceAsStream(classPathResource);
		if (in == null) {
			throw new RuntimeException("Script not found: " + classPathResource);
		}
		loadScript(in, classPathResource);
		int lastSlash = classPathResource.lastIndexOf('/');
		int ext = classPathResource.indexOf(".rb");
		String rubyClassName = classPathResource.substring(lastSlash + 1, ext);
		return ruby.evalScript(rubyClassName + ".new");
	}

	/**
	 * Checks to see if the scripts timestamp has been modified since the last
	 * invocation.
	 * 
	 * @param script
	 *            The script to check if it was modified.
	 */
	protected void loadScriptIfNecessary(File script) {
		String scriptName = script.getAbsolutePath();
		if (!timestamps.containsKey(scriptName)) {
			timestamps.putIfAbsent(scriptName, script.lastModified());
			ruby.loadFile(script);
		}
	}

	/**
	 * This method loads the script and if need be, tears down the previous
	 * JRuby Runtime instance that is existing. TODO: Check if Ruby#tearDown and
	 * Ruby#loadFile methods are thread safe.
	 * 
	 * @param script
	 *            The script to load.
	 */
	protected void loadScript(InputStream in, String actionName) {
		ruby.evalScript(new InputStreamReader(in), actionName);
		// TODO add timestamp checking back in
		// String scriptName = script.getAbsolutePath();
		// if (!timestamps.containsKey(scriptName)) {
		// timestamps.putIfAbsent(scriptName, script.lastModified());
		// ruby.loadFile(script);
		// } else {
		// long lastmodified = timestamps.get(scriptName);
		// if (lastmodified != script.lastModified()) {
		// ruby.tearDown();
		// ruby.loadFile(script);
		// timestamps.put(scriptName, script.lastModified());
		// }
		// }
	}

	/**
	 * TODO: Is this logic correct?
	 */
	public Object buildAction(String actionName, String namespace,
			ActionConfig actionConfig, Map context) throws Exception {
		Object action;
		String className = actionConfig.getClassName();
		if (className.startsWith("{") && className.endsWith("}")) {
			className = className.substring(1, className.length() - 1);
			action = getBeanInstance(className, actionConfig, context);
		} else {
			action = super.buildAction(namespace, actionName, actionConfig,
					context);
		}
		return action;
	}

	public Ruby getRuntime() {
		return ruby;
	}

	public String getBasePackage() {
		return basePackage;
	}

	@Inject
	public void setBasePackage(
			@Inject(value = "struts.jruby.basePackage", required = false)
			String basePackage) {
		this.basePackage = basePackage;
	}

	public boolean isAutoReload() {
		return autoReload;
	}

	@Inject
	public void setAutoReload(
			@Inject(value = "struts.jruby.autoReload", required = false)
			String autoReload) {
		this.autoReload = Boolean.parseBoolean(autoReload);
	}
}
